/**
 * @description socket语音服务
 */

import Recorder, { AudioProcessing } from '@/libs/recorder'
import { uuid } from '@/utils/random'
import { isAudioDB } from '@/utils/buffer'

export const SocketIO = window.io

export interface ASRIOOptions {
  /**
   * @description 连接建立成功
   */
  socketOptions?: LooseObject
  onConnect?: (event?: ASRIOMessage) => LooseObject
  onDisconnect?: (event?: ASRIOMessage) => LooseObject
  onStart?: (event?: ASRIOMessage) => LooseObject
  onError?: (event?: ErrorData) => LooseObject
  onMessage?: (event?: ASRIOMessage) => LooseObject
  onAudioProcessing?: (data?: AudioProcessing) => LooseObject
  onComplete?: (data?: AudioProcessing) => LooseObject
  url?: string
  debug?: boolean
}

export interface ASRIOMessage {
  error_code: number
  error_message: string
  finished: boolean
  restart: boolean
  text: string
}

export enum ASRIOReadstate {
  Connecting = 'Connecting', // 连接中
  ConnectTimeout = 'ConnectTimeout', // 连接超时
  Connect = 'Connect', // 已连接
  Authentication = 'Authentication', // 认证中
  Authenticated = 'Authenticated', // 已认证
  AuthFailed = 'AuthFailed', // 认证失败
  Reporting = 'Reporting', // 通信中
  Timeout = 'Timeout', // 超时未收到语音
  Error = 'Error', // 错误
  Disconnecting = 'Disconnecting', // 关闭中
  Disconnect = 'Disconnect', // 已关闭
}

export type ErrorData = Pick<any, 'message'>

export enum Apis {
  auth = 'session_info',
  message = 'message',
}

export enum EventNames {
  start = 'start',
  stop = 'stop',
  buffer = 'buffer',
}

/**
 * 一句话识别：(http/wss)nls_recognizer.xmov.ai
 * 实时语音识别：(http/wss)nls_transcriber.xmov.ai
 */
export const socketDefaultConfig = {
  url: 'nls_transcriber.xmov.ai' || '192.168.88.253:8701',
  protocols: 'http',
}

export default class ASRIO {
  private url: string = socketDefaultConfig.url // socket-io连接地址
  private options: ASRIOOptions // 配置项，包括众多回调
  private debug: boolean | undefined // 是否debug模式，debug模式将打印日志
  private tag = '[::ASRIO::]' // 模块标识
  private sessionId: string = uuid() // 服务通信id - 单轮内不变
  private reconnectAttemptLimit = 5 // 重连次数限制
  private reconnectAttemptCount = 0 // 已重连次数
  private reconnectTimeout = 1e4 // 连接超时时常 - 10s

  readstate: ASRIOReadstate = ASRIOReadstate.Connecting

  textResults: string[] = [] // 已收到的文本信息

  socket: LooseObject // socket-io 对象
  recorder: Recorder // 录音机

  /**
   * 最后接收到有效音频的时间
   */
  private __LAST_END_AUDIO_PROCESSING__: number = null

  /**
   * 刚启动时：可等待3秒
   * 已保有音频时：1秒不说话自动断开
   */
  private __INTERRUPT_AUDIO_PROCESSING_LIMIT__ = 3000

  constructor(options: ASRIOOptions) {
    this.options = options
    this.debug = options.debug
    this.url = options?.url || this.url
  }

  private isTimeout(): boolean {
    if (this.__LAST_END_AUDIO_PROCESSING__ === null) {
      this.__LAST_END_AUDIO_PROCESSING__ = Date.now()
      return false
    }
    const flag = Date.now() - this.__LAST_END_AUDIO_PROCESSING__ > this.__INTERRUPT_AUDIO_PROCESSING_LIMIT__
    if (flag) {
      this.setReadstate(ASRIOReadstate.Timeout)
    }
    return flag
  }

  private setReadstate(readstate: ASRIOReadstate) {
    this.log('[setReadstate]', readstate)
    this.readstate = readstate
  }

  private resetAudioProcessingLimit(number: number) {
    this.__INTERRUPT_AUDIO_PROCESSING_LIMIT__ = ~~number
  }

  async setup() {
    this.setReadstate(ASRIOReadstate.Connecting)
    return this.getRecord()
      .then(() => this.getSocket())
      .catch((error) => {
        this.onError(error)
        this.socket?.close()
      })
  }

  private log(...messages) {
    if (!this.debug) return
    console.log.call(window, this.tag, ...messages)
  }

  private error(...messages) {
    console.error.call(window, this.tag, ...messages)
  }

  async sendMessage(eventNames: EventNames, data: LooseObject = {}) {
    this.log('[socket send]', eventNames, data)
    return new Promise((resolve, reject) => {
      if (!this.socket) {
        reject()
      } else {
        const timer = setTimeout(() => {
          clearTimeout(timer)
          reject(true)
        }, 5e3)
        this.socket.compress(true).emit(
          Apis.message,
          {
            session_id: this.sessionId,
            data: eventNames,
            ...data,
          },
          (data: LooseObject) => {
            this.log('[socket sended]', data)
            clearTimeout(timer)
            resolve(data)
          },
        )
      }
    })
  }

  private async auth() {
    this.setReadstate(ASRIOReadstate.Authentication)
    return new Promise((resolve, reject) => {
      if (!this.socket) {
        reject()
      } else {
        this.socket.emit(Apis.auth, { session_id: this.sessionId }, (/* open ok */) => {
          this.sendMessage(EventNames.start)
            .then((data) => {
              this.onStart(data)
              this.setReadstate(ASRIOReadstate.Authenticated)
              resolve(data)
            })
            .catch(() => {
              this.onError(null)
              this.setReadstate(ASRIOReadstate.AuthFailed)
            })
        })
      }
    })
  }

  private async getRecord() {
    return new Promise((resolve, reject) => {
      this.log('[getRecord]')
      const asr = this as LooseObject
      Recorder.get(
        (recorder: Recorder) => {
          this.log('[getRecorder successful]', recorder)
          this.recorder = recorder
          // this.recorder.start()
          resolve(recorder)
        },
        (message) => {
          const data = { message }
          this.error('[getRecorder failed]: ', message)
          reject(data)
        },
        {
          onAudioProcessing: asr.onAudioProcessing.bind(asr),
          onComplete: asr.onComplete.bind(asr),
        },
      )
    })
  }

  private async getSocket() {
    return new Promise((resolve, reject) => {
      if (this.socket) {
        resolve(this.socket)
      } else {
        this.socket = SocketIO(this.url, {
          protocols: socketDefaultConfig.protocols,
          reconnectionDelay: 100,
          reconnectionAttempts: this.reconnectAttemptLimit,
        })
        let timeout = true
        const timer = setTimeout(() => {
          clearTimeout(timer)
          if (timeout) {
            const message = { message: '连接超时，请重试' }
            this.onError(message)
            this.setReadstate(ASRIOReadstate.ConnectTimeout)
            reject(message)
          }
        }, this.reconnectTimeout)
        this.socket.on('reconnect_attempt', () => {
          this.setReadstate(ASRIOReadstate.Connecting)
          this.reconnectAttemptCount++
          if (this.reconnectAttemptCount > this.reconnectAttemptLimit) {
            const error = { message: '连接超时，请重试' }
            reject(error)
            return error
          }
        })
        this.socket.on('connect', () => {
          this.log('[socket connect]')
          clearTimeout(timer)
          timeout = false
          this.onConnect().then(() => resolve(true))
          this.setReadstate(ASRIOReadstate.Connect)
        })
        this.socket.on('disconnect', (data: LooseObject) => {
          this.log('[socket disconnect]', data)
          this.onDisconnect()
        })
        this.socket.on('error', (error: Error) => {
          this.log('[error]', error)
          this.onError(error)
        })
      }
    })
  }

  disconnect() {
    this.setReadstate(ASRIOReadstate.Disconnecting)
    this.recorder?.stop?.()
    this.sendMessage(EventNames.stop).finally(() => {
      this.socket?.close?.()
    })
  }

  /**
   * 音频流数据更新
   * 回执音频信息，Siri绘制音频波纹
   * @param data
   */
  private onAudioProcessing(data: AudioProcessing) {
    const { buffer, peak } = data
    this.options.onAudioProcessing?.(data)
    if (!isAudioDB(buffer, peak)) {
      this.__LAST_END_AUDIO_PROCESSING__ = Date.now()
    } else {
      if (this.isTimeout()) {
        this.recorder?.stop()
        this.onComplete(data)
      }
    }
    this.sendMessage(EventNames.buffer, { data: buffer })
  }

  /**
   * 连接成功后绑定`message`事件
   */
  private async onConnect() {
    this.setReadstate(ASRIOReadstate.Connect)
    return this.auth().then(() => {
      this.socket.on('message', (data: LooseObject) => {
        try {
          data = JSON.parse(data)
        } catch {
          // ignore...
        }
        this.log('[socket message]', data)
        this.onMessage(data)
      })
    })
  }

  private onDisconnect() {
    this.setReadstate(ASRIOReadstate.Disconnect)
    this.options.onDisconnect?.()
  }

  private onStart(event?: LooseObject) {
    this.recorder?.start()
    this.options.onStart?.(event)
  }

  private onError(error?: ErrorData) {
    this.setReadstate(ASRIOReadstate.Error)
    this.disconnect()
    this.options.onError?.(error)
    this.error(error)
  }

  private onMessage(data: LooseObject) {
    this.setReadstate(ASRIOReadstate.Reporting)
    this.textResults.push(data.text)
    if (data.text) {
      this.resetAudioProcessingLimit(1000)
    }
    this.options.onMessage?.(data as ASRIOMessage)
  }

  private onComplete(data: AudioProcessing) {
    this.disconnect()
    this.options.onComplete?.(data)
  }
}
